﻿namespace EveKillboard.Services.EveKillServices
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using Core.Services;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Converters;
    using System.Diagnostics;
    using Core;
    using Core.Interfaces;

    public sealed class KillmailFeedProvider : IKillmailFeedServices, ITraceSource
    {
        public enum IdTypesEnum
        {
            NotDefined = 0,
            Character,
            Corporation,
            Alliance
        }

        const uint MAX_LIMIT = 500;

        /// <summary>
        /// 
        /// </summary>
        private enum OutputOptionsEnum
        {
            NotDefined = 0,
            /// <summary>
            /// Show the URL to EVE-KILL.
            /// </summary>
            Url = 1,
            /// <summary>
            /// Show the Timestamp.
            /// </summary>
            TimeStamp = 2,
            /// <summary>
            /// Show the internal EVE-KILL kill ID.
            /// </summary>
            InternalId = 4,
            /// <summary>
            /// Show the CCP API kill ID.
            /// </summary>
            CcpId = 8,
            /// <summary>
            /// Show the Victim Name.
            /// </summary>
            VictimName = 16,
            /// <summary>
            /// Show the Victim External ID.
            /// </summary>
            VictimId = 32,
            /// <summary>
            /// Show the Victim Corp Name.
            /// </summary>
            VictimCorporationName = 64,
            /// <summary>
            /// Show the Victim Alliance Name.
            /// </summary>
            VictimAllianceName = 128,
            /// <summary>
            /// Show the Victim Ship Name.
            /// </summary>
            VictimShipName = 256,
            /// <summary>
            /// Show the Victim Ships Class Name.
            /// </summary>
            VictimnShipClassName = 512,
            /// <summary>
            /// Show the Victim Ships External ID.
            /// </summary>
            VictimShipId = 1024,
            /// <summary>
            /// Show the FB Pilots name.
            /// </summary>
            FinalBlowPilotName = 2048,
            /// <summary>
            /// Show the FB Pilots corp name.
            /// </summary>
            FinalBlowPilotCorporationName = 4096,
            /// <summary>
            /// Show the FB Pilots alliance name.
            /// </summary>
            FinalBlowPilotAllianceName = 8192,
            /// <summary>
            /// Show the count of involved pilots.
            /// </summary>
            InvolvedCount = 16384,
            /// <summary>
            /// Show the Solar System the kill happened in.
            /// </summary>
            System = 32768,
            /// <summary>
            /// Show the Solar System true security.
            /// </summary>
            SystemSecurity = 65536,
            /// <summary>
            /// Show the Regions name.
            /// </summary>
            RegionName = 131072,
            /// <summary>
            /// Show the ISK value of the kill (total loss (ship + modules)).
            /// </summary>
            TotalValue = 262144,
            /// <summary>
            /// Show the pilots involved. 
            /// </summary>
            InvolvedPilots = 524288,
            /// <summary>
            /// Show the items that was destroyed and dropped.
            /// </summary>
            Items = 1048576,
            /// <summary>
            /// Default output option
            /// </summary>
            Default = TimeStamp | InternalId | CcpId | VictimName | VictimId | VictimCorporationName | VictimAllianceName | VictimShipId | System | InvolvedPilots | Items
        }

        /*
         * https://forums.eveonline.com/default.aspx?g=posts&t=18842
         * 
            mask // Required to get specific information from the API, look below for bitmask values of all commands
            mailLimit // Limits amount of mails show, minimum 1, maximum 500, default is 100
            week // Get kills for a specific week
            year // Get kills for a specific year
            startDate // Specifies a start date (YYYY-MM-DD H.i.s) (Replace : with .) (Space is _) Example: 2012-04-17_12.34.00 (17th April 2012 at Twelve Thirty Four)
            endDate // Specifies and end date (Should only be used if there is a startDate, and it cannot span months)
            APIOnly // set it to true, to only show mails that are externally verified (API obv.)
            KLLid // Shows information from a single kllid, can be either internal or external id (In theory anyway)
            minKLLid // The lowest kllid you want (Works best with maxkllid to get a specific range of mails)
            maxKLLid // The high kllid you want (Works best with minkllid to get a specific range of mails)
            minISKValue // Minimum isk value of kills
            maxISKValue // Maximum isk value of kills
            victimPilotExtID // Victim pilot external ID
            victimCorpExtID // Victim corp external ID
            victimAllianceExtID // Victim alliance external ID
            victimPilotID // Victim pilot internal ID
            victimCorpID // Victim corp internal ID
            victimAllianceID // Victim alliance internal ID
            victimPilot // Victim pilot name
            victimCorp // Victim corp name
            victimAlliance // Victim alliance name
            victimShip // Victim ship
            victimShipClass // Victim Ship Class
            combinedPilotExtID // Show Kills and Losses for Pilot external ID
            combinedCorpExtID // Show Kills and Losses for Corporation external ID
            combinedAllianceExtID // Show Kills and Losses for Alliance external ID
            combinedPilotID // Show Kills and Losses for Pilot internal ID
            combinedCorpID // Show Kills and Losses for Corporation internal ID
            combinedAllianceID // Show Kills and Losses for Alliance internal ID
            combinedPilot // Show Kills and Losses for Pilot name
            combinedCorp // Show Kills and Losses for Corporation name
            combinedAlliance // Show Kills and Losses for Alliance name
            involvedPilotExtID // Show kills with an involved pilot external ID
            involvedCorpExtID // Show kills with an involved corp external ID
            involvedAllianceExtID // Show kills with an involved alliance external ID
            involvedPilotID // Show kills with an involved pilot internal ID
            involvedCorpID // Show kills with an involved corp internal ID
            involvedAllianceID // Show kills with an involved alliance internal ID
            involvedPilot // Show kills with an involved pilot name
            involvedCorp // Show kills with an involved corp name
            involvedAlliance // Show kills with an involved alliance name
            involvedShip // Show kills with a involved ship
            involvedShipClass // Show kills with a involved ship class
            FBPilotName // Shows kills ONLY related to that finalblow pilot
            NoTowers // Removes towers, tower mods, territory modules, customs offices and infrastructure modules
            system // Show kills done in a system
            region // Show kills done in a region
        */

        private JsonSerializer _serializer = null;

        public KillmailFeedProvider()
        {
            Trace = new TraceSource("EveKillServices");

            _serializer = JsonSerializer.Create(new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore });
            _serializer.Converters.Add(new FeedKillmailConverter());
            _serializer.Converters.Add(new FeedKillmailInvolvedConverter());
            _serializer.Converters.Add(new FeedKillmailInvolvedListConverter());
            _serializer.Converters.Add(new DroppedKillmailItemListConverter());
            _serializer.Converters.Add(new DestroyedKillmailItemListConverter());
#if DEBUG
            _serializer.Error += SerializationErrorHandler;
#endif
        }

        ~KillmailFeedProvider()
        {
#if DEBUG
            if(_serializer !=null)
                _serializer.Error -= SerializationErrorHandler;
#endif
            _serializer = null;

            this.TraceEvent(TraceEventType.Verbose, "KillmailFeedProvider finalized");
        }

        public TraceSource Trace { get; set; }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
#if DEBUG
            if(_serializer !=null)
                _serializer.Error -= SerializationErrorHandler;
#endif
            _serializer = null;
            this.TraceEvent(TraceEventType.Verbose, "KillmailFeedProvider disposed");
        }

        private string GetContent(string uri)
        {
            if(string.IsNullOrEmpty(uri))throw new ArgumentNullException("uri");
            this.TraceEvent(TraceEventType.Verbose,"Requesting content from \"{0}\"", uri);
            try
            {
                var request = (HttpWebRequest)WebRequest.Create(uri);
                request.KeepAlive = false;

                var response = request.GetResponse();
                var st = response.GetResponseStream();
                if (st == null) return null;

                using (st)
                using (var reader = new StreamReader(st))
                    return reader.ReadToEnd();
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Error,"The http request with uri \"{2}\" has thrown an exception of type \"{0}\". Details: {1}", exp.GetType(), exp.Message, uri);
                return null;
            }
        }
        private List<IFeedKillmail> DeserializeContent(string content)
        {
            if(string.IsNullOrEmpty(content))throw new ArgumentNullException("content");
            this.TraceEvent(TraceEventType.Verbose,"Deserializing content \"{0}\"", content);
            try
            {
                using (var jreader = new JsonTextReader(new StringReader(content)))
                    return _serializer.Deserialize<List<IFeedKillmail>>(jreader);
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Error,"The attempt to deserialize content \"{2}\" has thrown an exception of type \"{0}\". Details: {1}", exp.GetType(), exp.Message, content);
                return null;
            }
        }

        public IEnumerable<IFeedKillmail> GetKillmails(int id, IdTypesEnum idType, uint max = MAX_LIMIT)
        {
            return GetKillmails(id, idType, null, null, max > MAX_LIMIT ? MAX_LIMIT : max );
        }

        public IEnumerable<IFeedKillmail> GetKillmails(int id, IdTypesEnum idType, DateTime? startDate, DateTime? endDate, uint max = MAX_LIMIT)
        {
            this.TraceEvent(TraceEventType.Verbose,"Retrieving killmails Type:{0} Id:{1} StartDate:{2} EndDate:{3} MaxLimit:{4}", idType, id, startDate.HasValue ? startDate.Value.ToLongDateString() : "None", endDate.HasValue ? endDate.Value.ToLongDateString() : "None", max);
            var result = new List<IFeedKillmail>();
            try
            {
                if(max > MAX_LIMIT)
                    max = MAX_LIMIT;

                string idArgs = null;
                bool hasDates = startDate.HasValue && endDate.HasValue;

                switch (idType)
                {
                    case IdTypesEnum.Alliance :
                        idArgs = "combinedAllianceExtID";
                        break;
                    case IdTypesEnum.Corporation:
                        idArgs = "combinedCorpExtID";
                        break;
                    case IdTypesEnum.Character :
                        idArgs = "combinedPilotExtID";
                        break;
                }

                if(idArgs ==null) return result;

                string uri = Uri.EscapeUriString(string.Format("http://eve-kill.net/epic/{3}:{1}/mask:{0}/mailLimit:{2}/{4}", (Int32)OutputOptionsEnum.Default, id, max, idArgs, hasDates ? string.Format("startDate:{0:yyyy-MM-dd}/endDate:{1:yyyy-MM-dd}", startDate.Value, endDate.Value) : null));

                string content = GetContent(uri);
                if (string.IsNullOrEmpty(content) || string.Equals("[]", content))
                {
                    this.TraceEvent(TraceEventType.Verbose, "No content received or content is empty");
                    return result;
                }

                var kills = DeserializeContent(content);
                if (kills != null && kills.Any())
                {
                    result.AddRange(kills.Where(k => !result.Contains(k)));
                    if (hasDates)
                        while (kills.Count == max)
                        {
                            uri = Uri.EscapeUriString(string.Format("http://eve-kill.net/epic/{4}:{1}/mask:{0}/mailLimit:{5}/startDate:{2:yyyy-MM-dd}/endDate:{3:yyyy-MM-dd}", (Int32)OutputOptionsEnum.Default, id, startDate, kills.Min(k => k.Date), idArgs, max));
                            var temp = GetContent(uri);
                            if (string.IsNullOrEmpty(temp) || string.Equals("[]", temp))
                            {
                                this.TraceEvent(TraceEventType.Verbose, "No content received or content is empty");
                                break;
                            }

                            kills = DeserializeContent(temp);
                            if (kills == null) break;
                            result.AddRange(kills.Where(k => !result.Contains(k)));
                        }
                }
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Error,"The attempt to get killmails has thrown an exception of type \"{0}\". Details: {1}", exp.GetType(), exp.Message);
            }
            ClearNames(result);
            return result;
        }
        
        public IEnumerable<IFeedKillmail> GetKillmails(int[] allianceIds)
        {
            var result = new List<IFeedKillmail>();
            try
            {
                foreach (var allianceId in allianceIds)
                {
                    var uri = Uri.EscapeUriString(string.Format("http://eve-kill.net/epic/combinedAllianceExtID:{1}/mask:{0}", (Int32)OutputOptionsEnum.Default, allianceId));

                    var request = (HttpWebRequest)WebRequest.Create(uri);
                    request.KeepAlive = false;
                    request.Timeout = 10000;

                    var response = request.GetResponse();
                    var st = response.GetResponseStream();
                    if(st ==null) continue;

                    string content = null;
                    using (st)
                    using (var reader = new StreamReader(st))
                        content = reader.ReadToEnd();

                    using (var jreader = new JsonTextReader(new StringReader(content)))
                    {
                        var kills = _serializer.Deserialize(jreader, typeof (List<IFeedKillmail>)) as List<IFeedKillmail>;
                        if (kills != null && kills.Any())
                            result.AddRange(kills);
                    }
                }
                
            }
            catch (Exception)
            {
            }
            ClearNames(result);
            return result;
        }

        private static void ClearNames(IEnumerable<IFeedKillmail> kills)
        {
            if(kills!=null)
                foreach (var kill in kills)
                {
                    kill.VictimAllianceName = GetClearName(kill.VictimAllianceName);
                    kill.VictimFactionName = GetClearName(kill.VictimFactionName);

                    foreach (var involved in kill.Involved)
                    {
                        involved.AllianceName = GetClearName(involved.AllianceName);
                        involved.FactionName = GetClearName(involved.FactionName);
                    }
                }
        }

        private static string GetClearName(string name)
        {
            var temp = string.IsNullOrEmpty(name)? null : name.Replace("None", null).Replace("none", null).Replace("Unknown", null).Replace("unknown",null).Trim();
            return string.IsNullOrEmpty(temp) ? null : temp;
        }

        public IEnumerable<IFeedKillmail> GetKillmails()
        {
            var result = new List<IFeedKillmail>();
            try
            {
              
                var uri = Uri.EscapeUriString(string.Format("http://eve-kill.net/epic/combinedAllianceExtID:{1}/mask:{0}", (Int32)OutputOptionsEnum.Default, 499005583));
                
                var request = (HttpWebRequest) WebRequest.Create(uri);
                request.KeepAlive = false;
                request.Timeout = 10000;

                var response = request.GetResponse();
                var st = response.GetResponseStream();
                if (st == null) return result;

                string content = null;
                using(st)
                using (var reader = new StreamReader(st))
                    content = reader.ReadToEnd();

                using(var jreader = new JsonTextReader(new StringReader(content)))
                    result = _serializer.Deserialize(jreader, typeof(List<IFeedKillmail>)) as List<IFeedKillmail>;    
            }
            catch (Exception)
            {
            }
            return result;
        }

#if DEBUG
        private void SerializationErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs e)
        {
            this.TraceEvent(TraceEventType.Warning,"A deserialization error occured. Type: {0} Message: {1}",e.ErrorContext.Error.GetType(), e.ErrorContext.Error.Message);
            //// If no items are dropped or destroyed (e.g capsule kill) 
            //// The items property is an empty array and not a null object.
            //e.ErrorContext.Handled = true;
        }
#endif
        private class FeedKillmailConverter : CustomCreationConverter<IFeedKillmail>
        {
            public override IFeedKillmail Create(Type objectType)
            {
                return new FeedKillmail();
            }
        }

        private class FeedKillmailInvolvedConverter : CustomCreationConverter<IFeedKillmailInvolved>
        {
            public override IFeedKillmailInvolved Create(Type objectType)
            {
                return new FeedKillmailInvolved();
            }
        }
        private class FeedKillmailInvolvedListConverter : CustomCreationConverter<IEnumerable<IFeedKillmailInvolved>>
        {
            public override IEnumerable<IFeedKillmailInvolved> Create(Type objectType)
            {
                return new List<FeedKillmailInvolved>();
            }
        }
        private class DroppedKillmailItemListConverter : CustomCreationConverter<IEnumerable<DroppedKillmailItem>>
        {
            public override IEnumerable<DroppedKillmailItem> Create(Type objectType)
            {
                return new List<DroppedKillmailItem>();
            }
        }
        private class DestroyedKillmailItemListConverter : CustomCreationConverter<IEnumerable<DestroyedKillmailItem>>
        {
            public override IEnumerable<DestroyedKillmailItem> Create(Type objectType)
            {
                return new List<DestroyedKillmailItem>();
            }
        }


        [JsonObjectAttribute(MemberSerialization.OptIn)]
        internal class FeedKillmail : IFeedKillmail
        {
            [JsonProperty(PropertyName = "timestamp")]
            public DateTime Date { get; set; }
            [JsonProperty(PropertyName = "internalID")]
            public int InternalId { get; set; }
            [JsonProperty(PropertyName = "externalID")]
            public int CcpId { get; set; }
            [JsonProperty(PropertyName = "victimExternalID")]
            public int VictimId { get; set; }
            [JsonProperty(PropertyName = "victimName")]
            public string VictimName { get; set; }
            [JsonProperty(PropertyName = "victimCorpName")]
            public string VictimCorporationName { get; set; }
            [JsonIgnore]
            public int VictimCorporationId { get; set; }
            [JsonProperty(PropertyName = "victimAllianceName")]
            public string VictimAllianceName { get; set; }
            [JsonIgnore]
            public int VictimAllianceId { get; set; }
            [JsonProperty(PropertyName = "victimFactionName")]
            public string VictimFactionName { get; set; }
            [JsonIgnore]
            public int VictimFactionId { get; set; }
            [JsonProperty(PropertyName = "victimShipID")]
            public int VictimShipId { get; set; }
            [JsonIgnore]
            public string VictimShipName { get; set; }
            [JsonProperty(PropertyName = "solarSystemName")]
            public string SystemName { get; set; }
            [JsonIgnore]
            public int SystemId { get; set; }
            [JsonProperty(PropertyName = "items")]
            public KillmailItems KillItems { get; set; }
            [JsonProperty(PropertyName = "involved")]
            public IEnumerable<IFeedKillmailInvolved> Involved { get; set; }
            [JsonIgnore]
            public IEnumerable<IFeedKillmailItem> Items
            {
                get
                {
                    var items = new List<FeedKillmailItem>();
                    if (KillItems == null) return items;

                    items.AddRange(
                        KillItems.Destroyed.Select(
                            i =>
                            new FeedKillmailItem()
                                {
                                    ItemId = i.ItemId,
                                    Quantity = i.Quantity,
                                    IsDropped = false,
                                    IsDestroyed = true,
                                    IsCargo = i.ItemSlot == "4", // i.Flag == 5,
                                    IsDroneBay = i.ItemSlot == "6", // i.Flag == 87,
                                    IsImplant = i.ItemSlot == "8", // i.Flag == 89,
                                    IsCopy = false
                                }));

                    items.AddRange(
                        KillItems.Dropped.Select(
                            i =>
                            new FeedKillmailItem()
                                {
                                    ItemId = i.ItemId,
                                    Quantity = i.Quantity,
                                    IsDropped = true,
                                    IsDestroyed = false,
                                    IsCargo = i.ItemSlot == "4", // i.Flag == 5,
                                    IsDroneBay = i.ItemSlot == "6", // i.Flag == 87,
                                    IsImplant = i.ItemSlot == "8", // i.Flag == 89,
                                    IsCopy = false
                                }));

                    return items;
                }
                set { }
            }
            [JsonIgnore]
            public int DamageReceived
            {
                get { return Involved.Sum(i => i.DamageDone); }
                set { }
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof (FeedKillmail)) return false;
                return Equals((FeedKillmail) obj);
            }

            public bool Equals(FeedKillmail other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return other.InternalId == InternalId;
            }

            public override int GetHashCode()
            {
                return InternalId;
            }
        }

        [JsonObjectAttribute(MemberSerialization.OptIn)]
        internal class KillmailItems
        {
            [JsonProperty("destroyed")]
            public IEnumerable<DestroyedKillmailItem> Destroyed { get; set; }
            [JsonProperty("dropped")]
            public IEnumerable<DroppedKillmailItem> Dropped { get; set; }
        }

        [JsonObjectAttribute(MemberSerialization.OptIn)]
        internal class DestroyedKillmailItem
        {
            [JsonProperty("typeID")]
            public int ItemId { get; set; }
            [JsonProperty("qtyDestroyed")]
            public int Quantity { get; set; }
            [JsonProperty("itemSlot")]
            public string ItemSlot { get; set; }
        }
        [JsonObjectAttribute(MemberSerialization.OptIn)]
        internal class DroppedKillmailItem
        {
            [JsonProperty("typeID")]
            public int ItemId { get; set; }
            [JsonProperty("qtyDropped")]
            public int Quantity { get; set; }
            [JsonProperty("itemSlot")]
            public string ItemSlot { get; set; }
        }

        [JsonObjectAttribute(MemberSerialization.OptIn)]
        internal class FeedKillmailInvolved : IFeedKillmailInvolved
        {
            [JsonProperty(PropertyName = "characterID")]
            public int CharacterId { get; set; }
            [JsonProperty(PropertyName = "characterName")]
            public string CharacterName { get; set; }
            [JsonProperty(PropertyName = "corporationID")]
            public int CorporationId { get; set; }
            [JsonProperty(PropertyName = "corporationName")]
            public string CorporationName { get; set; }
            [JsonProperty(PropertyName = "allianceID")]
            public int AllianceId { get; set; }
            [JsonProperty(PropertyName = "allianceName")]
            public string AllianceName { get; set; }
            [JsonProperty(PropertyName = "factionID")]
            public int FactionId { get; set; }
            [JsonProperty(PropertyName = "factionName")]
            public string FactionName { get; set; }
            [JsonProperty(PropertyName = "securityStatus")]
            public float SecurityStatus { get; set; }
            [JsonProperty(PropertyName = "damageDone")]
            public int DamageDone { get; set; }
            [JsonProperty(PropertyName = "finalBlow")]
            public bool IsFinalBlow { get; set; }
            [JsonProperty(PropertyName = "shipTypeID")]
            public int ShipId { get; set; }
            [JsonIgnore]
            public string ShipName { get; set; }
            [JsonProperty(PropertyName = "weaponTypeID")]
            public int WeaponId { get; set; }
            [JsonIgnore]
            public string WeaponName { get; set; }
        }
        internal class FeedKillmailItem : IFeedKillmailItem
        {
            public int ItemId { get; set; }
            public string ItemName { get; set; }
            public int Quantity { get; set; }
            public bool IsDestroyed { get; set; }
            public bool IsDropped { get; set; }
            public bool IsCargo { get; set; }
            public bool IsDroneBay { get; set; }
            public bool IsImplant { get; set; }
            public bool IsCopy { get; set; }
        }
    }
}